查看原文
其他

Java多线程编程-(9)-使用线程池实现线程的复用和一些坑的避免

徐刘根 Java后端技术 2019-01-09

前几篇:

Java多线程编程-(1)-线程安全和锁Synchronized概念

Java多线程编程-(2)-可重入锁以及Synchronized的其他基本特性

Java多线程编程-(3)-从一个错误的双重校验锁代码谈一下volatile关键字

Java多线程编程-(4)-线程本地ThreadLocal的介绍与使用

Java多线程编程-(5)-线程间通信机制的介绍与使用

Java多线程编程-(6)-你还在使用wait/notify实现进程间的通信吗?

Java多线程编程-(7)-使用ReentrantReadWriteLock实现Lock并发

Java多线程编程-(8)-两种常用的线程计数器CountDownLatch和循环屏障CyclicBarrier

线程复用:线程池

首先举个例子:

假设这里有一个系统,大概每秒需要处理5万条数据,这5万条数据为一个批次,而这没秒发送的5万条数据数据需要经过两个处理过程,第一步是数据存入数据库,第二步是对数据进行其他业务的分析,假设第一步我是用的是普通的JDBC插入数据,为了不影响程序的继续执行,我写了一个线程,让这个子线程不阻塞主线程,继续处理第二步骤的数据,我们知道插入5万条数据大概需要2至3秒的时间,如果每一批次插入数据库的时候,就创建一个线程进行处理,可想而知,由于插入数据库的时间较久,不能很快的处理,这样的话,一段时间之后,系统中就会有很多的这种插入数据的线程(PS:只是假设场景,方案设计的可能不合理)。

如果,我们使用上述的方式去创建线程,使用start()方法启动线程,该线程会在run()方法结束后,自动回收该线程。虽然如此,在上边的场景中线程中业务的处理速度完全达不到我们的要求,系统中的线程会逐渐变大,进而消耗CPU资源,大量的线程抢占宝贵的内存资源,可能还会出现OOM,即便没有出现,大量的线程回收也会个GC带来很大的压力。

可想而知,虽然多线程技术可以充分发挥多核处理器的计算能力,提高生产系统的吞吐量和性能。但是,若不加控制和管理的随意使用线程,对系统的性能反而会产生不利的影响。

还拿上边的例子说,如果我们使用线程池的方式的话,可以实现最多创建爱你线程的数量,这样的话就算再多的数据需要入库,只需要排队等待线程池的线程即可,就不会出现线程池过多而消耗系统资源的情况,当然这只是意见简单的场景。

说到这里,有人要说了线程不是携带资源的最小单位,操作系统的书籍中还给我们说了线程之间的切换消耗很小吗?虽然如此,线程是一种轻量级的工具(或者称之为:轻量级进程),但其创建和关闭依然需要花费时间,如果为了一个很简单的任务就去创建一个线程,很有可能出现创建和销毁线程所占用的时间大于该线程真实工作所消耗的时间,反而得不偿失。

那么什么是线程池?

为了避免系统频繁的创建和销毁线程,我们可以将创建的线程进行复用。数据库中的数据库连接池也是此意。

在线程池中总有那么几个活跃的线程,也有一定的最大值限制,一个业务使用完线程之后,不是立即销毁而是将其放入到线程池中,从而实现线程的复用。简而言之:创建线程变成了从线程池获取空闲的线程,关闭线程变成了向池子中归还线程。

再多的概念,不过多解释,因为很基础,也不是本文的重点。

JDK对线程池的支持

JDK提供的Eexecutor框架

JDK提供了Executor框架,可以让我们有效的管理和控制我们的线程,其实质也就是一个线程池。Executor下的接口和类继承关系如下:

其中,ExecutorService接口定义如下:

如果使用Executor框架的话,Executors类是常用的,其方法如下:

其中常用几类如下:

1、newFixedThreadPool:该方法返回一个固定线程数量的线程池;

2、newSingleThreadExecutor:该方法返回一个只有一个现成的线程池;

3、newCachedThreadPool:返回一个可以根据实际情况调整线程数量的线程池;

4、newSingleThreadScheduledExecutor:该方法和newSingleThreadExecutor的区别是给定了时间执行某任务的功能,可以进行定时执行等;

5、newScheduledThreadPool:在4的基础上可以指定线程数量。

创建线程池是指调用的还是ThreadPoolExecutor

在Executors类中,我们拿出来一个方法简单分析一下:

可以看出,类似的其他方法一样,在Executors内部创建线程池的时候,实际创建的都是一个ThreadPoolExecutor对象,只是对ThreadPoolExecutor构造方法,进行了默认值的设定。ThreadPoolExecutor的构造方法如下:

参数含义如下:

Eexecutor框架实例

1、实例一:

submit(Runnable task)方法提交一个线程。

但是使用最新的“阿里巴巴编码规范插件”检测一下会发现:

阿里巴巴编码规范插件地址:https://github.com/alibaba/p3c

2、实例二:

遵循阿里巴巴编码规范的提示,示例如下:

或者这样:

3、实例三:

自定义ThreadFactory、自定义线程拒绝策略

更多实例代码,可参考:

https://gitee.com/xuliugen/codes/ta5dbsge0kvhy62qu8li157

使用submit的坑

首先看一下实例:

运行结果:

上述代码,可以看出运行结果为4个,因该是有5个的,但是当i=0的时候,100/0是会报错的,但是日志信息中没有任何信息,是为什么那?如果使用了submit(Runnable task) 就会出现这种情况,任何的错误信息都出现不了!

这是因为使用submit(Runnable task) 的时候,错误的堆栈信息跑出来的时候会被内部捕获到,所以打印不出来具体的信息让我们查看,解决的方法有如下两种:

1、使用execute()代替submit();

运行结果:

2、使用Future

运行结果:

注:查看源代码请点击阅读原文,PC端效果更佳!


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存